#ifndef SPLINTER_PPFORMBUILDER_H
#define SPLINTER_PPFORMBUILDER_H

#include "datatable.h"
#include "ppform.h"
#include "cinterface/ISampler.h"
#include "boundary.h"

namespace SPLINTER
{

    // ppForm smoothing
    enum class PPForm::Smoothing
    {
        NONE,     // No smoothing
        IDENTITY, // Regularization term alpha*c'*I*c is added to OLS objective
        PSPLINE   // Smoothing term alpha*Delta(c,2) is added to OLS objective
    };

    // ppForm knot spacing
    /*
     * To be added:
     * AS_SAMPLED_NOT_CLAMPED   // Place knots close to sample points. Without clamps.
     * EQUIDISTANT_NOT_CLAMPED  // Equidistant knots without clamps.
     */
    enum class PPForm::KnotSpacing
    {
        AS_SAMPLED,  // Mimic spacing of sample points (moving average). With clamps (p+1 multiplicity of end knots).
        EQUIDISTANT, // Equidistant knots. With clamps (p+1 multiplicity of end knots).
        EXPERIMENTAL // Experimental knot spacing (for testing purposes).
    };

    // ppForm builder class
    class SPLINTER_API PPForm::Builder
    {
    public:
        Builder(const DataTable &data);

        Builder &alpha(double alpha)
        {
            if (alpha < 0)
                throw Exception("PPForm::Builder::alpha: alpha must be non-negative.");

            _alpha = alpha;
            return *this;
        }

        // Set build options

        Builder &degree(unsigned int degree)
        {
            _degrees = getPPFormDegrees(_data.getNumVariables(), degree);
            return *this;
        }

        Builder &degree(std::vector<unsigned int> degrees)
        {
            if (degrees.size() != _data.getNumVariables())
                throw Exception("PPForm::Builder: Inconsistent length on degree vector.");
            _degrees = degrees;
            return *this;
        }

        Builder &numBasisFunctions(unsigned int numBasisFunctions)
        {
            _numBasisFunctions = std::vector<unsigned int>(_data.getNumVariables(), numBasisFunctions);
            return *this;
        }

        Builder &numBasisFunctions(std::vector<unsigned int> numBasisFunctions)
        {
            if (numBasisFunctions.size() != _data.getNumVariables())
                throw Exception("PPForm::Builder: Inconsistent length on numBasisFunctions vector.");
            _numBasisFunctions = numBasisFunctions;
            return *this;
        }

        Builder &knotSpacing(KnotSpacing knotSpacing)
        {
            _knotSpacing = knotSpacing;
            return *this;
        }

        Builder &smoothing(Smoothing smoothing)
        {
            _smoothing = smoothing;
            return *this;
        }

        Builder &bondary(const Boundary &bondary, const ISampler *sampler = nullptr)
        {
            _bondary = bondary;
            if (bondary == Boundary::COMPLETE || bondary == Boundary::SECOND_DERIVATIVES_AT_END || bondary == Boundary::PERIODIC)
            {
                if (sampler)
                    this->sampler = sampler;
                else
                    throw Exception("PPForm::Builder: when bondary=\"" + std::string(BoundaryHelper::convertBoundaryString(bondary)) + "\", sampler is needed!");
            }
            return *this;
        }

        // Build ppForm
        PPForm build() const;

    private:
        Builder();

        std::vector<unsigned int> getPPFormDegrees(unsigned int numVariables, unsigned int degree)
        {
            if (degree > 5)
                throw Exception("PPForm::Builder: Only degrees in range [0, 5] are supported.");
            return std::vector<unsigned int>(numVariables, degree);
        }

        // Control point computations
        std::vector<Polynomial> computePolynomials(const PPForm &ppForm) const;
        DenseVector computePPFormCoefficients(const PPForm &ppForm) const;
        SparseMatrix computeBasisFunctionMatrix(const PPForm &ppForm) const;
        DenseVector getSamplePointValues() const;
        // P-spline control point calculation
        SparseMatrix getSecondOrderFiniteDifferenceMatrix(const PPForm &ppForm) const;

        // Computing knots
        std::vector<std::vector<double>> computeKnotVectors() const;
        std::vector<double> computeKnotVector(const std::vector<double> &values, unsigned int degree, unsigned int numBasisFunctions) const;
        std::vector<double> knotVectorMovingAverage(const std::vector<double> &values, unsigned int degree) const;
        std::vector<double> knotVectorEquidistant(const std::vector<double> &values, unsigned int degree, unsigned int numBasisFunctions) const;
        std::vector<double> knotVectorBuckets(const std::vector<double> &values, unsigned int degree, unsigned int maxSegments = 10) const;

        // Auxiliary
        std::vector<double> extractUniqueSorted(const std::vector<double> &values) const;

        // Member variables
        DataTable _data;
        std::vector<unsigned int> _degrees;
        std::vector<unsigned int> _numBasisFunctions;
        KnotSpacing _knotSpacing;
        Smoothing _smoothing;
        double _alpha;
        Boundary _bondary;
        const ISampler *sampler;
    };

} // namespace SPLINTER

#endif // SPLINTER_PPFORMBUILDER_H
